﻿using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
using System.Text;
using Newtonsoft.Json.Linq;

namespace betfairfind
{
    /// <summary>
    /// Betfair API via Betfair website.
    /// </summary>
    /// <author>pdonald</author>
    public class BetfairWebsite // : IBetfair
    {
        WebClient browser = new WebClient() { AllowAutoRedirect = false, Timeout = TimeSpan.FromSeconds(2), MaxAttempts = 5, ConcurrentConnections = 10 };
        Dictionary<DateTime, int> calls = new Dictionary<DateTime, int>();

        public BetfairWebsite()
        {
            MaxChargableRequestsPerSecond = 5; // 10 calls per second
        }

        /// <summary>
        /// Max number of requests to make to the Betfair website per second.
        /// Note that each request is weighted 2.
        /// </summary>
        public int MaxChargableRequestsPerSecond
        {
            get;
            set;
        }

        public bool Login(string username, string password)
        {
            try
            {
                string data = "username=" + Uri.EscapeDataString(username) + "&password=" + Uri.EscapeDataString(password);

                browser.Get("http://www.betfair.com/");
                browser.Post("https://accountservices.betfair.com/account/login/LoginRequest.do", data);

                return browser.Response.Headers["Location"].Contains("LoginResponse.do?status=success");
            }
            catch
            {
                return false;
            }
        }

        public MarketPricesCompressed GetBestPrices(int marketId)
        {
            lock (browser) Wait(); // block until it's ok to make a request so that we don't get charged

            if (marketId == 102000589)
            {
                Console.Write("");
            }
            try
            {
                string response = browser.Get("http://uk.site.sports.betfair.com/betting/api/json/getFullMarketData.do?mi=" + marketId + "&prevcache=0").Replace("while(1) {};\n", "");
                JObject json = JObject.Parse(response);

                MarketPricesCompressed prices = new MarketPricesCompressed(null);
                prices.MarketId = (int)json["market"]["id"];
                prices.Timestamp = DateTime.Now;
                prices.InPlayDelay = (int)json["market"]["betDelay"];
                prices.RemovedRunnerInfo = (string)json["rules"]["runnersRemoved"];
                prices.CurrencyCode = ((string)json["market"]["currency"]).Replace("&nbsp;", "");
                prices.NumberOfWinners = (int)json["market"]["numberOfWinners"];
                prices.MarketInfo = (string)json["rules"]["marketInformation"];
                prices.DiscountAllowed = (bool)json["market"]["discountAllowed"];
                prices.MarketBaseRate = float.Parse((string)json["market"]["commission"], CultureInfo.InvariantCulture);
                prices.BspMarket = (bool)json["market"]["bspMarket"];
                prices.MarketStatus = (string)json["market"]["status"] == "A" ? Betfair.UK.MarketStatusEnum.ACTIVE : Betfair.UK.MarketStatusEnum.SUSPENDED;

                string matched = ((string) json["market"]["totalMoneyMatched"]).Split(new string[] {"&nbsp;"}, StringSplitOptions.None)[1];
                prices.TotalMatched = float.Parse(matched, CultureInfo.InvariantCulture);

                var runners = (JArray)json["runners"];
                var runnerprices = (JArray)json["runnerprices"];

                for (int i = 0; i < runners.Count; i++)
                {
                    var selection = new MarketPricesCompressed.SelectionPricesInfo();
                    selection.SelectionId = (int)runners[i]["selectionId"];
                    selection.OrderIndex = i + 1;
                    selection.Vacant = (bool)runners[i]["vacant"];

                    for (int j = 0; j < 3; j++)
                    {
                        double odds = (double)runnerprices[i]["bestBacks"][j]["price"];
                        selection.PriceInfo.Add(odds,
                            new MarketPricesCompressed.SelectionPricesInfo.OddsAvailablePriceInfo()
                            {
                                Odds = odds,
                                BackAmountAvailable = (double)runnerprices[i]["bestBacks"][j]["size"]
                            });

                        odds = (double)runnerprices[i]["bestLays"][j]["price"];
                        selection.PriceInfo.Add(odds,
                            new MarketPricesCompressed.SelectionPricesInfo.OddsAvailablePriceInfo()
                            {
                                Odds = odds,
                                LayAmountAvailable = (double)runnerprices[i]["bestLays"][j]["size"]
                            });
                    }

                    prices.CompletePriceInfo.Add(selection);
                }

                return prices;
            }
            catch
            {
                return new MarketPricesCompressed(null);
            }
        }

        /// <summary>
        /// Blocks the thread until it's safe to make a request
        /// if the number of requests made in this second exceed MaxChargableRequestsPerSecond.
        /// </summary>
        /// <notes>
        /// Can probably be rewritten with a queue. 
        /// In the meantime you can set up a break point and take a look at calls and make sure that 
        /// at no point in time did the client exceed the limits.
        /// </notes>
        private void Wait()
        {
            bool wait = true;

            while (wait)
            {
                DateTime now = DateTime.Now;
                now = new DateTime(now.Year, now.Month, now.Day, now.Hour, now.Minute, now.Second);

                int callsThisSecond = calls.ContainsKey(now) ? calls[now] : 0;
                wait = callsThisSecond >= MaxChargableRequestsPerSecond;

                if (!wait)
                {
                    calls[now] = callsThisSecond + 1;

                    //foreach (var kp in calls.Where(kp => kp.Key < now).ToArray())
                        //calls.Remove(kp.Key);

                    return;
                }

                System.Threading.Thread.Sleep(TimeSpan.FromMilliseconds(50));
            }
        }
    }

    /// <summary>
    /// Custom WebClient which supports cookies.
    /// </summary>
    /// <author>pdonald</author>
    internal class WebClient : IDisposable
    {
        public WebClient()
        {
            AllowAutoRedirect = true;
            Timeout = TimeSpan.FromSeconds(10);
            MaxAttempts = 1;
            Cookies = new CookieCollection();
            ConcurrentConnections = 10;
        }

        public bool AllowAutoRedirect { get; set; }
        public TimeSpan Timeout { get; set; }
        public int MaxAttempts { get; set; }
        public CookieCollection Cookies { get; private set; }
        public HttpWebResponse Response { get; private set; }
        public int ConcurrentConnections { get { return ServicePointManager.DefaultConnectionLimit; } set { ServicePointManager.DefaultConnectionLimit = value; } }

        public string Get(string address)
        {
            for (int i = 0; i < MaxAttempts; i++)
            {
                try
                {
                    return GetInternal(address);
                }
                catch (WebException)
                {
                    // if e == timeout?
                }
            }

            return null;
        }

        public string Post(string address, string data)
        {
            for (int i = 0; i < MaxAttempts; i++)
            {
                try
                {
                    return PostInternal(address, data);
                }
                catch (WebException)
                {
                    // if e == timeout?
                }
            }

            return null;
        }

        private string GetInternal(string address)
        {
            HttpWebRequest request = GetRequest(address);
            request.Method = "GET";
            return GetResponse(request);
        }

        private string PostInternal(string address, string data)
        {
            byte[] bytes = Encoding.UTF8.GetBytes(data);

            HttpWebRequest request = GetRequest(address);
            request.Method = "POST";
            request.ContentType = "application/x-www-form-urlencoded";
            request.ContentLength = bytes.Length;
            
            using (Stream stream = request.GetRequestStream())
                stream.Write(bytes, 0, bytes.Length);

            return GetResponse(request);
        }

        protected virtual HttpWebRequest GetRequest(string address)
        {
            HttpWebRequest request = (HttpWebRequest)WebRequest.Create(address);
            request.AllowAutoRedirect = AllowAutoRedirect;
            request.Timeout = (int)Timeout.TotalMilliseconds;
            request.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate;
            request.KeepAlive = false;
            request.CookieContainer = new CookieContainer();
            request.CookieContainer.Add(Cookies);
            return request;
        }

        protected virtual string GetResponse(HttpWebRequest request)
        {
            Response = (HttpWebResponse)request.GetResponse();
            Cookies.Add(Response.Cookies);

            using (StreamReader reader = new StreamReader(Response.GetResponseStream()))
                return reader.ReadToEnd();
        }

        public void Dispose()
        {
            if (Response != null)
                ((IDisposable)Response).Dispose();
        }
    }
}
